package editor;

import editor.code.LineField;
import editor.headers.Header;
import editor.headers.HeaderComponent;
import editor.widgets.CustomScrollPane;
import editor.widgets.FindBar;
import editor.widgets.ObservedStringTable;
import event.UndoSet;
import event.Undoable;
import model.caret.Caret;
import model.caret.CaretRef;
import model.caret.HeaderCaret;
import model.caret.TextCaret;
import model.code.*;
import model.code.line.Line;
import model.code.line.LineList;
import model.code.line.MultiLineList;
import model.symbol.*;
import util.Pair;
import widgets.CustomSplitPane;
import widgets.CustomTabPane;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;

/**
 * @author Neil Dickson
 */
public class UIController {
	private final PwnIDE			editor;
	private final CustomSplitPane	split;
	private final CustomTabPane		tabs;
	private FindBar					findBar;

	private int			selectedIndex;
	private CodeFile	currentFile;

	UIController(PwnIDE editor, CustomSplitPane split, CustomTabPane tabs) {
		this.editor = editor;
		this.split = split;
		this.tabs = tabs;
		selectedIndex = -1;
		currentFile = null;
		findBar = null;
	}

	/**
	 * Gets the Header at the specified index into the tabs.
	 * @param tabIndex index into the {@link CustomTabPane}
	 * @return the Header if there is a Header there, else null
	 */
	public Header getTabHeader(int tabIndex) {
		if (tabIndex>=tabs.getTabCount() || tabIndex<0) {
			return null;
		}
		Component c = ((Container)((CustomScrollPane)tabs.getTabAt(tabIndex)).getComponent()).getComponent(0);
		if (c instanceof Header) {
			return (Header)c;
		}
		return null;
	}

	public ProjectFile getFileFromComponent(Component c) {
		return (ProjectFile)((Header)((JPanel)((CustomScrollPane)c).getComponent()).getComponent(0)).getCode();
	}

	public void showOpenFiles() {
		if (editor.getProject()!=null) {
			for (ProjectFile file : editor.getProject().getOpenFiles()) {
				if (file!=null) {
					openFile(file);
				}
			}
			if (editor.getProject().getActiveFile()!=null) {
				openFile(editor.getProject().getActiveFile());
			}
		}
	}
	/**
	 * This function opens a file in a new tab, or switches to the tab with the file already open.
	 *
	 * @param file the file to be opened
	 */
	public void openFile(ProjectFile file) {
		openFile(file,true);
	}

	public void openFile(ProjectFile file,boolean moveToCaret) {
		for (int tab=0;tab<tabs.getTabCount();++tab) {
			Header header = getTabHeader(tab);
			if (header!=null && header.getCode()==file) {
				tabs.setSelectedIndex(tab);
				updateFocus();
				return;
			}
		}
		final JPanel panel = new JPanel(null);
		final Header header;
		if (file instanceof CodeFile) {
			header = new Header((CodeFile)file);
			panel.add(header);
			panel.setBackground(header.getBackground());
		}
		else {
			throw new NotImplementedException("Sorry, displaying non-code files isn't implemented yet.");
		}
		final CustomScrollPane scroll = new CustomScrollPane(panel);
		panel.getParent().addComponentListener(header);
		tabs.addTab(file.toString(),file.getIcon(),scroll);
		tabs.setSelectedComponent(scroll);
		header.doFullLayout();

		JPopupMenu tabMenu = new JPopupMenu();
		JMenuItem closeMenuItem = new JMenuItem("Close Tab");
		closeMenuItem.addActionListener(new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				closeTab(tabs.getComponentIndex(scroll));
			}
		});
		tabMenu.add(closeMenuItem);
		JMenuItem closeAllButMenuItem = new JMenuItem("Close All But This");
		closeAllButMenuItem.addActionListener(new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				closeAllTabsBut(tabs.getComponentIndex(scroll));
			}
		});
		tabMenu.add(closeAllButMenuItem);
		JMenuItem closeAllMenuItem = new JMenuItem("Close All Tabs");
		closeAllMenuItem.addActionListener(new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				closeAllTabs();
			}
		});
		tabMenu.add(closeAllMenuItem);
		JMenuItem closeAllUnmodifiedMenuItem = new JMenuItem("Close All Unmodified Tabs");
		closeAllUnmodifiedMenuItem.addActionListener(new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				ErrorHandler.showNotImplementedMessage("Sorry, but the current version doesn't check for modifications, so closing all unmodified tabs isn't implemented.");
			}
		});
		tabMenu.add(closeAllUnmodifiedMenuItem);
		tabs.setTabMenu(tabs.getTabCount()-1,tabMenu);

		// Move to the caret if supposed to, otherwise, just update the focus to make sure that something still has focus
		if (file instanceof CodeFile && moveToCaret && ((CodeFile)file).getCaretRef().get()!=null) {
			goToCaret(((CodeFile)file).getCaretRef().get());
		}
		else {
			updateFocus();
		}
	}

	public void closeTab(int index) {
		tabs.removeTabAt(index);
	}
	public void closeAllTabs() {
		tabs.removeAll();
	}
	public void closeAllTabsBut(int index) {
		for (int i=tabs.getTabCount()-1;i>=0;--i) {
			if (i!=index) {
				tabs.removeTabAt(i);
			}
		}
	}
	public CodeFile getCurrentCodeFile() {
		return currentFile;
	}
	public Header  getCurrentCodeFileHeader() {
		return getTabHeader(selectedIndex);
	}
	public CodeFileChunk getCurrentChunk() {
		return (currentFile!=null) ? getCurrentChunk(currentFile) : null;
	}
	public CodeFileChunk getCurrentChunk(CodeFile file) {
		CaretRef ref = file.getCaretRef();
		Caret caret = ref.get();
		if (caret==null) {
			return null;
		}
		if (caret instanceof TextCaret) {
			TextCaret textCaret = (TextCaret)caret;
			return textCaret.getLineList().getParent();
		}
		if (caret instanceof HeaderCaret) {
			return ((HeaderCaret)caret).getLast();
		}
		return null;
	}
	public Undoable addContentAfterCurrent(CodeFileChunk chunk) {
		if (currentFile!=null) {
			CodeFileChunk currentChunk = getCurrentChunk(currentFile);
			Section parent = (currentChunk!=null) ? (currentChunk.getParent()) : null;
			if (parent!=null) {
				if (!(chunk instanceof Section)) {
					if (!(currentChunk instanceof Section)) {
						return parent.addAfter(chunk,currentChunk);
					}
					return ((Section)currentChunk).add(chunk);
				}
				// This case is that chunk is a Section, and currentChunk may or may not be a Section
				Section newSection = (Section)chunk;
				int startSrcIndex = parent.getContent().indexOf(currentChunk);
				UndoSet undoSet = new UndoSet(new Undoable[0],"Adding Section");
				for (int i=startSrcIndex+1;i<parent.getContent().size() && !(parent.getContent().get(i) instanceof Section);) {
					CodeFileChunk tempChunk = parent.getContent().get(i);
					undoSet.add(parent.remove(i));
					undoSet.add(newSection.add(tempChunk));
				}
				if (parent instanceof CodeFile) {
					undoSet.add(parent.addAfter(newSection,currentChunk));
				}
				else {
					undoSet.add(parent.getParent().addAfter(newSection,parent));
				}
				return undoSet;
			}
			if (!(chunk instanceof Section) && !currentFile.getContent().isEmpty() && (currentFile.getContent().getLast() instanceof Section)) {
				return ((Section)currentFile.getContent().getLast()).add(chunk);
			}
			return currentFile.add(chunk);
		}
		return null;
	}

	public void goToDefinition(Symbol symbol) {
		try {
			if (symbol instanceof CodeFileChunk) {
//				goToChunkHeader((CodeFileChunk)symbol);
				goToCaret(new TextCaret(null,symbol.getName(),0,0,0));
			}
			else if (symbol instanceof StructureMember || symbol instanceof Constant || symbol instanceof LocalVariable) {
				goToCaret(new TextCaret(null,symbol.getName(),0,0,0));
			}
			else if (symbol instanceof LineLabel) {
				LineLabel label = (LineLabel)symbol;
				if (editor.getProject()!=null && label.getParent()!=null) {
					Function function = label.getParent();
					outer_loop:
					for (int i=0;i<function.getCode().size();++i) {
						Line line = function.getCode().get(i);
						Object[] references = line.getReferences();
						boolean[] isDefiningReference = line.getIsDefiningReference();
						int nReferences = line.getNumReferences();
						for (int j=0;j<nReferences;++j) {
							if (references[j]==symbol && isDefiningReference[j]) {
								goToCaret(new TextCaret(null,function.getCode(),i,line.getTokenColumn(j),0));
								break outer_loop;
							}
						}
					}
				}
			}
		}
		catch (Throwable t) {
			ErrorHandler.showException(t,"Sorry, an error occurred while trying to go to definition.\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
		}
	}

	/**
	 * Gets the Header and (x,y) coordinates of the specified CodeFileChunk.
	 * NOTE: It will expand content to get a location for the Header, but won't open the file if it's not open.
	 * @param chunk the CodeFileChunk to look for
	 * @return the Header and its (x,y) coordinates, or null if the file is not open or the chunk is not in a file.
	 */
	public Pair<Header,Point> getHeaderAndLocation(CodeFileChunk chunk) {
		final CodeFile file = chunk.getFile();
		if (file==null) {
			return null;
		}
		Header fileHeader = null;
		for (int tab=0;tab<tabs.getTabCount();++tab) {
			Header header = getTabHeader(tab);
			if (header!=null && header.getCode()==file) {
				fileHeader = header;
				break;
			}
		}
		if (fileHeader==null) {
			return null;
		}
		Section section = chunk.getParent();
		Header sectionHeader = fileHeader;
		Point p = sectionHeader.getLocation();
		if (chunk instanceof CodeFile) {
			return new Pair<Header,Point>(sectionHeader,p);
		}
		if (section!=file) {
			fileHeader.getContentPanel().setExpanded(true);
			p.x += fileHeader.getContentPanel().getX();
			p.y += fileHeader.getContentPanel().getY();
			Header[] subHeaders = fileHeader.getContentPanel().getContent();
			for (Header subHeader : subHeaders) {
				if (subHeader.getCode()==section) {
					sectionHeader = subHeader;
					p.x += subHeader.getX();
					p.y += subHeader.getY();
					break;
				}
			}
		}
		sectionHeader.getContentPanel().setExpanded(true);
		p.x += sectionHeader.getContentPanel().getX();
		p.y += sectionHeader.getContentPanel().getY();
		Header[] subHeaders = sectionHeader.getContentPanel().getContent();
		for (Header subHeader : subHeaders) {
			if (subHeader.getCode()==chunk) {
				p.x += subHeader.getX();
				p.y += subHeader.getY();
				return new Pair<Header,Point>(subHeader,p);
			}
		}
		// NOTE: This shouldn't happen, but might if the chunk is removed from the CodeFile.
		return null;
	}
	public void goToChunkHeader(CodeFileChunk chunk) {
		final CodeFile file = chunk.getFile();
		if (file.isExternal()) {
			ErrorHandler.showNotImplementedMessage("Sorry, \"Go To Definition\" isn't implemented for things in external includes yet.\nThis is partly because displaying windows.inc takes a ton of memory.");
			return;
		}
		if (file!=currentFile) {
			openFile(file,false);
		}
		Header fileHeader = getCurrentCodeFileHeader();
		Pair<Header,Point> pair = getHeaderAndLocation(chunk);
		CustomScrollPane scroll = ((CustomScrollPane)tabs.getTabAt(tabs.getSelectedIndex()));
		// NOTE: scrollTo() is preferred over scrollToRectVisible() here, because the view should always move such that the header is at the top (excepting of course for the end of the file).
		scroll.scrollTo(0,Math.min(pair.b.y,fileHeader.getHeight()-scroll.getViewport().height));
		//scroll.scrollToRectVisible(p.x,p.y,header.getWidth(),header.getHeight());
	}

	public LineField getLineField(LineList lines) {
		CodeFileChunk chunk = lines.getParent();
		if (chunk==null) {
			return null;
		}
		final CodeFile file = chunk.getFile();
		if (!file.getProject().getOpenFiles().contains(file)) {
			return null;
		}
		Pair<Header,Point> pair = getHeaderAndLocation(chunk);
		if (pair==null || pair.a==null) {
			return null;
		}
		HeaderComponent component = pair.a.getComponentOfType(lines.getLocationType());
		LineField field = null;
		if (component!=null) {
			for (int i=0;i<component.getComponentCount();++i) {
				if (component.getComponent(i) instanceof LineField) {
					if (((LineField)component.getComponent(i)).getLines()==lines) {
						field = (LineField)component.getComponent(i);
						break;
					}
				}
				else if (component.getComponent(i) instanceof ObservedStringTable) {
					ObservedStringTable table = (ObservedStringTable)component.getComponent(i);
					field = table.getCell(lines.getLocationIndex()>>>16,lines.getLocationIndex()&0xFFFF);
					break;
				}
			}
		}
		return field;
	}

	public void updateFocus() {
		if (tabs.getSelectedIndex()!=selectedIndex) {
			selectedIndex = tabs.getSelectedIndex();
			Header currentFileHeader = getTabHeader(selectedIndex);
			currentFile = (currentFileHeader==null) ? null : (CodeFile)currentFileHeader.getCode();
		}
		if (currentFile!=null) {
			Caret caret = currentFile.getCaretRef().get();
			if (caret instanceof TextCaret) {
				LineList lines = ((TextCaret)caret).getLineList();
				if (lines!=null) {
					LineField field = getLineField(lines);
					if (field!=null) {
						field.requestFocus();
					}
				}
			}
		}
	}

	/**
	 * Moves the view such that the specified Caret is visible, including opening/switching files.
	 * NOTE: caret.caretRef can be null
	 * @param caret the caret to which to scroll
	 */
	public void goToCaret(Caret caret) {
		if (caret instanceof HeaderCaret) {
			HeaderCaret headerCaret = (HeaderCaret)caret;
			ArrayList<CodeFileChunk> chunks = headerCaret.getSelection();
			if (chunks.size()>0) {
				goToChunkHeader(chunks.get(0));
			}
		}
		else if (caret instanceof TextCaret) {
			TextCaret textCaret = (TextCaret)caret;
			LineList lines = textCaret.getLineList();
			CodeFileChunk chunk = lines.getParent();
			if (chunk==null) {
				return;
			}
			final CodeFile file = chunk.getFile();
			if (file.isExternal()) {
				ErrorHandler.showNotImplementedMessage("Sorry, \"Go To Definition\" isn't implemented for things in external includes yet.\nThis is partly because displaying windows.inc takes a ton of memory.");
				return;
			}
			if (file!=currentFile) {
				openFile(file,false);
			}
			LineField field = getLineField(lines);
			// Fallback is to go to the name
			if (field==null && lines.getParent()!=null) {
				field = getLineField(lines.getParent().getName());
			}
			if (field!=null) {
				field.requestFocus();
				CaretRef ref = file.getCaretRef();
				ref.set(textCaret);
				field.caretChanged(ref);
			}
			// Don't display the error if the project is loading
			else if (editor.isVisible() && editor.getProject()!=null) {
				ErrorHandler.showNotImplementedMessage("Sorry, for some reason I can't find the place I'm trying to go.");
			}
		}
	}

	public void createFunction() {
		if (currentFile!=null) {
			try {
				Function function = new Function("","",currentFile.getLanguage());
				Undoable event = addContentAfterCurrent(function);
				editor.eventDone(event);
				goToCaret(new TextCaret(null,function.getName(),0,0,0));
			}
			catch (Throwable t) {
				ErrorHandler.showException(t,"Sorry, an error occurred while trying to create a function.\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
			}
		}
	}
	public void createStructure() {
		if (currentFile!=null) {
			try {
				Structure structure = new Structure("","");
				structure.addMember(new StructureMember("","DWORD","?","",structure,0));
				Undoable event = addContentAfterCurrent(structure);
				editor.eventDone(event);
				goToCaret(new TextCaret(null,structure.getName(),0,0,0));
			}
			catch (Throwable t) {
				ErrorHandler.showException(t,"Sorry, an error occurred while trying to create a structure.\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
			}
		}
	}
	public void createVariable() {
		if (currentFile!=null) {
			try {
				GlobalVariable variable = new GlobalVariable("","");
				variable.getPieces().add(new GlobalVariable.Piece(variable.getDefaultType(),variable.getDefaultValue(),variable,0));
				Undoable event = addContentAfterCurrent(variable);
				editor.eventDone(event);
				goToCaret(new TextCaret(null,variable.getName(),0,0,0));
			}
			catch (Throwable t) {
				ErrorHandler.showException(t,"Sorry, an error occurred while trying to create a variable.\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
			}
		}
	}
	public void createConstants() {
		if (currentFile!=null) {
			try {
				Constants constants = new Constants("","");
				constants.add(new Constant("","","",constants,0));
				Undoable event = addContentAfterCurrent(constants);
				editor.eventDone(event);
				goToCaret(new TextCaret(null,constants.getName(),0,0,0));
			}
			catch (Throwable t) {
				ErrorHandler.showException(t,"Sorry, an error occurred while trying to create a set of constants.\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
			}
		}
	}
	public void createSection() {
		if (currentFile!=null) {
			try {
				Section section = new Section("","");
				Undoable event = addContentAfterCurrent(section);
				editor.eventDone(event);
				goToCaret(new TextCaret(null,section.getName(),0,0,0));
			}
			catch (Throwable t) {
				ErrorHandler.showException(t,"Sorry, an error occurred while trying to create a section.\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
			}
		}
	}

	public void toFindBar(String findText) {
		if (findBar==null) {
			findBar = new FindBar(this);
			//findBar.setLocation(0,split.getHeight()-findBar.getHeight());
			((MultiLineList)findBar.findTextField.getLines()).addObserver(findBar.findTextField);
		}
		if (findBar.getParent()!=editor.findAndTabs) {
			editor.findAndTabs.add(findBar,BorderLayout.SOUTH);
			editor.findAndTabs.doLayout();
		}
		LineList lines = findBar.findTextField.getLines();
		lines.replace(0,0,lines.size()-1,lines.get(lines.size()-1).length(),findText);
		CaretRef caretRef = findBar.getCaretRef();
		caretRef.set(new TextCaret(caretRef,lines,0,lines.get(0).length(),0));
		findBar.findTextField.requestFocus();
	}

	public void findReferences(Symbol symbol) {
		findReferences(symbol,true);
	}
	public ArrayList<TextCaret> findReferences(Symbol symbol,boolean displayResults) {
		ArrayList<TextCaret> carets = new ArrayList<TextCaret>();
		for (CodeFile file : editor.getProject().getDirectory().getAllFiles()) {
			for (LineList list = file.getFirstLineList(); list!=null; list = CodeFileChunk.getNextLineListPlus(list)) {
				findReferences(symbol,list,carets);
			}
		}
		if (displayResults) {
			PwnIDE.editor.showCaretList(carets);
		}
		return carets;
	}
	public void findReferences(Symbol symbol,LineList list,ArrayList<TextCaret> carets) {
		for (int i=0;i<list.size();++i) {
			Line line = list.get(i);
			for (int j=0; j<line.getNumReferences(); ++j) {
				if (line.getReferences()[j]==symbol) {
					carets.add(new TextCaret(null, list, i, line.getReferenceIndices()[j], 0));
				}
			}
		}
	}
}
